2 * Copyright (c) 2013-2014 Apple Inc. All Rights Reserved.
4 * @APPLE_LICENSE_HEADER_START@
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
21 * @APPLE_LICENSE_HEADER_END@
25 #import "KNAppDelegate.h"
26 #import "KDSecCircle.h"
27 #import "KDCirclePeer.h"
28 #import "NSDictionary+compactDescription.h"
29 #import <AOSUI/NSImageAdditions.h>
30 #import <AppleSystemInfo/AppleSystemInfo.h>
31 #import <Security/SecFrameworkStrings.h>
33 #import <AOSAccounts/MobileMePrefsCoreAEPrivate.h>
34 #import <AOSAccounts/MobileMePrefsCore.h>
36 static char *kLaunchLaterXPCName = "com.apple.security.Keychain-Circle-Notification-TICK";
37 static const NSString *kKickedOutKey = @"KickedOut";
38 static const NSString *kValidOnlyOutOfCircleKey = @"ValidOnlyOutOfCircle";
40 @implementation KNAppDelegate
42 static NSUserNotificationCenter *appropriateNotificationCenter()
44 return [NSUserNotificationCenter _centerForIdentifier:@"com.apple.security.keychain-circle-notification" type:_NSUserNotificationCenterTypeSystem];
47 -(void)notifyiCloudPreferencesAbout:(NSString *)eventName;
49 if (nil == eventName) {
53 NSString *account = (__bridge NSString *)(MMCopyLoggedInAccount());
54 NSLog(@"notifyiCloudPreferencesAbout %@", eventName);
57 BOOL createdAEDesc = createAEDescWithAEActionAndAccountID((__bridge NSString *)kMMServiceIDKeychainSync, eventName, account, &aeDesc);
61 LSLaunchURLSpec lsSpec;
64 lsSpec.itemURLs = (__bridge CFArrayRef)([NSArray arrayWithObject:[NSURL fileURLWithPath:@"/System/Library/PreferencePanes/iCloudPref.prefPane"]]);
65 lsSpec.passThruParams = &aeDesc;
66 lsSpec.launchFlags = kLSLaunchDefaults | kLSLaunchAsync;
67 lsSpec.asyncRefCon = NULL;
69 err = LSOpenFromURLSpec(&lsSpec, NULL);
72 NSLog(@"Can't send event %@, err=%d", eventName, err);
74 AEDisposeDesc(&aeDesc);
78 NSLog(@"unable to create and send aedesc for account: '%@' and action: '%@'\n", account, eventName);
82 -(void)showiCloudPrefrences
84 static NSAppleScript *script = nil;
86 script = [[NSAppleScript alloc] initWithSource:@"tell application \"System Preferences\"\n\
88 set the current pane to pane id \"com.apple.preferences.icloud\"\n\
92 NSDictionary *appleScriptError = nil;
93 [script executeAndReturnError:&appleScriptError];
95 if (appleScriptError) {
96 NSLog(@"appleScriptError: %@", appleScriptError);
98 NSLog(@"NO appleScript error");
104 NSDate *nowish = [NSDate new];
105 self.state = [KNPersistantState loadFromStorage];
106 if ([nowish compare:self.state.pendingApplicationReminder] != NSOrderedAscending) {
107 NSLog(@"REMINDER TIME: %@ >>> %@", nowish, self.state.pendingApplicationReminder);
108 // self.circle.rawStatus might not be valid yet
109 if (SOSCCThisDeviceIsInCircle(NULL) == kSOSCCRequestPending) {
110 // Still have a request pending, send reminder, and also in addtion to the UI
111 // we need to send a notification for iCloud pref pane to pick up
113 CFNotificationCenterPostNotificationWithOptions(CFNotificationCenterGetDistributedCenter(), CFSTR("com.apple.security.secureobjectsync.pendingApplicationReminder"), (__bridge const void *)([self.state.applcationDate description]), NULL, 0);
115 [self postApplicationReminder];
116 self.state.pendingApplicationReminder = [nowish dateByAddingTimeInterval:[self getPendingApplicationReminderInterval]];
117 [self.state writeToStorage];
122 -(void)scheduleActivityAt:(NSDate*)time
124 if ([time compare:[NSDate distantFuture]] != NSOrderedSame) {
125 NSTimeInterval howSoon = [time timeIntervalSinceNow];
127 [self scheduleActivityIn:howSoon];
134 -(void)scheduleActivityIn:(int)alertInterval
136 xpc_object_t options = xpc_dictionary_create(NULL, NULL, 0);
137 xpc_dictionary_set_uint64(options, XPC_ACTIVITY_DELAY, alertInterval);
138 xpc_dictionary_set_uint64(options, XPC_ACTIVITY_GRACE_PERIOD, XPC_ACTIVITY_INTERVAL_1_MIN);
139 xpc_dictionary_set_bool(options, XPC_ACTIVITY_REPEATING, false);
140 xpc_dictionary_set_bool(options, XPC_ACTIVITY_ALLOW_BATTERY, true);
141 xpc_dictionary_set_string(options, XPC_ACTIVITY_PRIORITY, XPC_ACTIVITY_PRIORITY_UTILITY);
143 xpc_activity_register(kLaunchLaterXPCName, options, ^(xpc_activity_t activity) {
148 -(NSTimeInterval)getPendingApplicationReminderInterval
150 if (self.state.pendingApplicationReminderInterval) {
151 return [self.state.pendingApplicationReminderInterval doubleValue];
157 - (void)applicationDidFinishLaunching:(NSNotification *)aNotification
159 appropriateNotificationCenter().delegate = self;
161 NSLog(@"Posted at launch: %@", appropriateNotificationCenter().deliveredNotifications);
163 self.viewedIds = [NSMutableSet new];
164 self.circle = [KDSecCircle new];
165 self.state = [KNPersistantState loadFromStorage];
166 KNAppDelegate *me = self;
168 [self.circle addChangeCallback:^{
169 me.state = [KNPersistantState loadFromStorage];
170 if ((me.state.lastCircleStatus == kSOSCCInCircle && !me.circle.isInCircle) || me.state.debugLeftReason) {
171 enum DepartureReason reason = kSOSNeverLeftCircle;
172 if (me.state.debugLeftReason) {
173 reason = [me.state.debugLeftReason intValue];
174 me.state.debugLeftReason = nil;
176 CFErrorRef err = NULL;
177 reason = SOSCCGetLastDepartureReason(&err);
178 if (reason == kSOSDepartureReasonError) {
179 NSLog(@"SOSCCGetLastDepartureReason err: %@", err);
183 //NSString *model = (__bridge NSString *)(ASI_CopyComputerModelName(FALSE));
184 NSString *body = nil;
186 case kSOSDepartureReasonError:
187 case kSOSNeverLeftCircle:
188 case kSOSWithdrewMembership:
192 NSLog(@"Unknown departure reason %d", reason);
193 // fallthrough on purpose
195 case kSOSMembershipRevoked:
196 case kSOSLeftUntrustedCircle:
197 body = NSLocalizedString(@"Approve this Mac from another device to use iCloud Keychain.", @"Body for iCloud Keychain Reset notification");
200 [me.state writeToStorage];
201 NSLog(@"departure reason %d, body=%@", reason, body);
203 [me postKickedOutWithMessage: body];
205 } else if (me.circle.isInCircle) {
206 // We are in a circle, so we should get rid of any reset notifications that are hanging out
207 NSUserNotificationCenter *noteCenter = appropriateNotificationCenter();
208 for (NSUserNotification *note in noteCenter.deliveredNotifications) {
209 if (note.userInfo[kValidOnlyOutOfCircleKey]) {
210 NSLog(@"Removing existing notification (%@) now that we are in circle", note);
211 [appropriateNotificationCenter() removeDeliveredNotification: note];
218 if (me.state.lastCircleStatus != kSOSCCRequestPending && me.circle.rawStatus == kSOSCCRequestPending) {
219 NSLog(@"Entered RequestPending");
220 NSDate *nowish = [NSDate new];
221 me.state.applcationDate = nowish;
222 me.state.pendingApplicationReminder = [me.state.applcationDate dateByAddingTimeInterval:[me getPendingApplicationReminderInterval]];
223 [me.state writeToStorage];
224 [me scheduleActivityAt:me.state.pendingApplicationReminder];
227 NSMutableSet *applicantIds = [NSMutableSet new];
228 for (KDCirclePeer *applicant in me.circle.applicants) {
229 if (!me.circle.isInCircle) {
230 // We don't want to yammer on about circles we aren't in,
231 // and we don't want to be extra confusing announcing our
232 // own join requests as if the user could approve them
236 [me postForApplicant:applicant];
237 [applicantIds addObject:applicant.idString];
240 NSUserNotificationCenter *notificationCenter = appropriateNotificationCenter();
241 NSLog(@"Checking validity of %lu notes", (unsigned long)notificationCenter.deliveredNotifications.count);
242 for (NSUserNotification *note in notificationCenter.deliveredNotifications) {
243 if (note.userInfo[@"applicantId"] && ![applicantIds containsObject:note.userInfo[@"applicantId"]]) {
244 NSLog(@"No longer an applicant (%@) for %@ (I=%@)", note.userInfo[@"applicantId"], note, [note.userInfo compactDescription]);
245 [notificationCenter removeDeliveredNotification:note];
247 NSLog(@"Still an applicant (%@) for %@ (I=%@)", note.userInfo[@"applicantId"], note, [note.userInfo compactDescription]);
251 me.state.lastCircleStatus = me.circle.rawStatus;
253 [me.state writeToStorage];
256 [me scheduleActivityAt:me.state.pendingApplicationReminder];
259 -(BOOL)userNotificationCenter:(NSUserNotificationCenter *)center shouldPresentNotification:(NSUserNotification *)notification
264 -(void)userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification
266 if (notification.activationType == NSUserNotificationActivationTypeActionButtonClicked) {
267 [self notifyiCloudPreferencesAbout:notification.userInfo[@"Activate"]];
270 // The "Later" seems handled Ok without doing anything here, but KickedOut & other special items need an action
271 if (notification.userInfo[@"SPECIAL"]) {
272 NSLog(@"ACTIVATED (remove): %@", notification);
273 [appropriateNotificationCenter() removeDeliveredNotification:notification];
275 NSLog(@"ACTIVATED (NOT removed): %@", notification);
279 -(void)userNotificationCenter:(NSUserNotificationCenter *)center didDismissAlert:(NSUserNotification *)notification
281 [self notifyiCloudPreferencesAbout:notification.userInfo[@"Dismiss"]];
283 if (!notification.userInfo[@"SPECIAL"]) {
284 // If we don't do anything here & another notification comes in we
285 // will repost the alert, which will be dumb.
286 id applicantId = notification.userInfo[@"applicantId"];
287 if (applicantId != nil) {
288 [self.viewedIds addObject:applicantId];
290 NSLog(@"DISMISS (t) %@", notification);
292 NSLog(@"DISMISS (f) %@", notification);
293 [appropriateNotificationCenter() removeDeliveredNotification:notification];
297 -(void)postForApplicant:(KDCirclePeer*)applicant
299 static int postCount = 0;
301 if ([self.viewedIds containsObject:applicant.idString]) {
302 NSLog(@"Already viewed %@, skipping", applicant);
306 NSUserNotificationCenter *noteCenter = appropriateNotificationCenter();
307 for (NSUserNotification *note in noteCenter.deliveredNotifications) {
308 if ([applicant.idString isEqualToString:note.userInfo[@"applicantId"]]) {
309 if (note.isPresented) {
310 NSLog(@"Already posted&presented: %@ (I=%@)", note, note.userInfo);
313 NSLog(@"Already posted, but not presented: %@ (I=%@)", note, note.userInfo);
318 NSUserNotification *note = [NSUserNotification new];
320 // Genstrings command line is: genstrings -o en.lproj -u KNAppDelegate.m
321 note.title = [NSString stringWithFormat:NSLocalizedString(@"iCloud Keychain", @"Title for new keychain syncing device notification")];
322 note.informativeText = [NSString stringWithFormat:NSLocalizedString(@"\\U201C%1$@\\U201D wants to use your passwords.", @"Message text for new keychain syncing device notification"), applicant.name];
324 note.hasActionButton = YES;
325 note._displayStyle = _NSUserNotificationDisplayStyleAlert;
326 note._identityImage = [NSImage bundleImage];
327 note._identityImageHasBorder = NO;
328 note._actionButtonIsSnooze = YES;
329 note.actionButtonTitle = NSLocalizedString(@"Later", @"Button label to dismiss device notification");
330 note.otherButtonTitle = NSLocalizedString(@"View", @"Button label to view device notification");
332 note.identifier = [[NSUUID new] UUIDString];
334 note.userInfo = @{@"applicantName": applicant.name,
335 @"applicantId": applicant.idString,
336 @"Dismiss": (__bridge NSString *)kMMPropertyKeychainAADetailsAEAction,
339 NSLog(@"About to post#%d/%lu (%@): %@", postCount, (unsigned long)noteCenter.deliveredNotifications.count, applicant.idString, note);
340 [appropriateNotificationCenter() deliverNotification:note];
345 -(void)postKickedOutWithMessage:(NSString*)body
347 NSUserNotificationCenter *noteCenter = appropriateNotificationCenter();
348 for (NSUserNotification *note in noteCenter.deliveredNotifications) {
349 if (note.userInfo[kKickedOutKey]) {
350 if (note.isPresented) {
351 NSLog(@"Already posted&presented (removing): %@", note);
352 [appropriateNotificationCenter() removeDeliveredNotification: note];
354 NSLog(@"Already posted, but not presented: %@", note);
359 NSUserNotification *note = [NSUserNotification new];
361 note.title = NSLocalizedString(@"iCloud Keychain Was Reset", @"Title for iCloud Keychain Reset notification");
362 note.informativeText = body; // Already LOCed
364 note._identityImage = [NSImage bundleImage];
365 note._identityImageHasBorder = NO;
366 note.otherButtonTitle = NSLocalizedString(@"Close", @"Close button");
367 note.actionButtonTitle = NSLocalizedString(@"Options", @"Options Button");
369 note.identifier = [[NSUUID new] UUIDString];
371 note.userInfo = @{kKickedOutKey: @1,
372 kValidOnlyOutOfCircleKey: @1,
374 @"Activate": (__bridge NSString *)kMMPropertyKeychainMRDetailsAEAction,
377 NSLog(@"About to post#-/%lu (KICKOUT): %@", (unsigned long)noteCenter.deliveredNotifications.count, note);
378 [appropriateNotificationCenter() deliverNotification:note];
381 -(void)postApplicationReminder
383 NSUserNotificationCenter *noteCenter = appropriateNotificationCenter();
384 for (NSUserNotification *note in noteCenter.deliveredNotifications) {
385 if (note.userInfo[@"ApplicationReminder"]) {
386 if (note.isPresented) {
387 NSLog(@"Already posted&presented (removing): %@", note);
388 [appropriateNotificationCenter() removeDeliveredNotification: note];
390 NSLog(@"Already posted, but not presented: %@", note);
395 NSUserNotification *note = [NSUserNotification new];
397 note.title = NSLocalizedString(@"iCloud Keychain", @"Title for iCloud Keychain Application still pending (from this device) reminder");
398 note.informativeText = NSLocalizedString(@"Approve this Mac from another device to use iCloud Keychain.", @"Body text for iCloud Keychain Application still pending (from this device) reminder");
400 note._identityImage = [NSImage bundleImage];
401 note._identityImageHasBorder = NO;
402 note.otherButtonTitle = NSLocalizedString(@"Close", @"Close button");
403 note.actionButtonTitle = NSLocalizedString(@"Options", @"Options Button");
405 note.identifier = [[NSUUID new] UUIDString];
407 note.userInfo = @{@"ApplicationReminder": @1,
408 kValidOnlyOutOfCircleKey: @1,
410 @"Activate": (__bridge NSString *)kMMPropertyKeychainWADetailsAEAction,
413 NSLog(@"About to post#-/%lu (REMINDER): %@ (I=%@)", (unsigned long)noteCenter.deliveredNotifications.count, note, [note.userInfo compactDescription]);
414 [appropriateNotificationCenter() deliverNotification:note];